/**
 * BibSonomy-Importer - Various importers for bookmarks and publications.
 *
 * Copyright (C) 2006 - 2016 Knowledge & Data Engineering Group,
 *                               University of Kassel, Germany
 *                               http://www.kde.cs.uni-kassel.de/
 *                           Data Mining and Information Retrieval Group,
 *                               University of Würzburg, Germany
 *                               http://www.is.informatik.uni-wuerzburg.de/en/dmir/
 *                           L3S Research Center,
 *                               Leibniz University Hannover, Germany
 *                               http://www.l3s.de/
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.bibsonomy.importer.bookmark.service;

import static org.bibsonomy.util.ValidationUtils.present;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bibsonomy.model.Bookmark;
import org.bibsonomy.model.Post;
import org.bibsonomy.model.Tag;
import org.bibsonomy.model.util.GroupUtils;
import org.bibsonomy.model.util.TagUtils;
import org.bibsonomy.services.importer.RelationImporter;
import org.bibsonomy.services.importer.RemoteServiceBookmarkImporter;
import org.bibsonomy.util.StringUtils;
import org.bibsonomy.util.io.xml.FilterInvalidXMLCharsReader;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * Imports bookmarks and relations from Delicious. To get an instance of this 
 * class, use the {@link DeliciousImporterFactory}.
 * 
 * @see "https://github.com/avos/delicious-api"
 * The doc writes:
 * "All /v1 api's require https requests and HTTP-Auth.
 * 
 * @author:  rja
 * 
 */
public class DeliciousImporter implements RemoteServiceBookmarkImporter, RelationImporter {

	private static final Log log = LogFactory.getLog(DeliciousImporter.class);
	
	private static final String DELICIOUS_DEFAULT_ENCODING = StringUtils.CHARSET_UTF_8;
	private static final String HEADER_USER_AGENT = "User-Agent";
	private static final String HEADER_AUTHORIZATION = "Authorization";
	private static final String HEADER_AUTH_BASIC = "Basic ";
	private static final DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");

	/** The URL to contact Delicious. */
	private final URL apiURL;
	private final String userAgent;


	private String password;
	private String userName;


	/**
	 * Constructor which allows to give a specific {@link #apiURL}.
	 * @param apiUrl - the URL to contact delicious
	 * @param userAgent - the userAgent this importer shall use to identify 
	 * itself in the corresponding HTTP header
	 */
	protected DeliciousImporter(final URL apiUrl, final String userAgent) {
		this.apiURL = apiUrl;
		this.userAgent = userAgent;
	}

	/**
	 * This Method retrieves a list of Posts for a given user.
	 */
	@Override
	public List<Post<Bookmark>> getPosts() throws IOException{
		
		final List<Post<Bookmark>> posts = new LinkedList<Post<Bookmark>>();
				
		//open a connection to delicious and retrieve a document
		Document document;
		try {
			document = getDocument();
		} catch (SAXException e1) {
			throw new IOException(e1);
		} catch (ParserConfigurationException e1) {
			throw new IOException(e1);
		}
		
		// traverse document and put everything into Post<Bookmark> Objects
		final NodeList postList = document.getElementsByTagName("post");
		for (int i = 0; i < postList.getLength(); i++) {
			final Element resource = (Element)postList.item(i);
			
			final Post<Bookmark> post = new Post<Bookmark>();
			final Bookmark bookmark = new Bookmark();
			
			// setting the url and the title. if the title is "", use the url as title.
			final String description = resource.getAttribute("description");
			final String href = resource.getAttribute("href");
			if (present(description)) { 
				bookmark.setTitle(description);
			} else {
				bookmark.setTitle(href);
			}
			bookmark.setUrl(href);
			try {
				post.getTags().addAll(TagUtils.parse(resource.getAttribute("tag")));
			} catch (Exception e) {
				throw new IOException("Could not parse tags. ", e);
			}
			
			// no tags available? -> add one tag to the resource and mark it as "imported"
			if (!present(post.getTags())) {
				post.setTags(Collections.singleton(TagUtils.getEmptyTag()));
			}
			
			post.setDescription(resource.getAttribute("extended"));
			try {
				post.setDate(fmt.parseDateTime(resource.getAttribute("time")).toDate());
			} catch (Exception e) {
				log.warn("Could not parse date.", e);
				post.setDate(new Date());
			}
			
			// set the visibility of the imported resource
			if (resource.hasAttribute("shared") && "no".equals(resource.getAttribute("shared"))) {
				post.getGroups().add(GroupUtils.buildPrivateGroup());
			} else {
				post.getGroups().add(GroupUtils.buildPublicGroup());
			}
			post.setResource(bookmark);
			posts.add(post);
		}
		
		return posts;
	}

	/**
	 * This method retrieves a list of tags with subTags from Delicious.
	 */
	@Override
	public List<Tag> getRelations() throws IOException {
		final List<Tag> relations = new LinkedList<Tag>();
		//open a connection to delicious and retrieve a document
		try {
			final Document document = getDocument();
			final NodeList bundles = document.getElementsByTagName("bundle");
			for(int i = 0; i < bundles.getLength(); i++){
				final Element resource = (Element)bundles.item(i);
				try {
					final Tag tag = new Tag(resource.getAttribute("name"));
					tag.getSubTags().addAll(TagUtils.parse(resource.getAttribute("tags")));
					relations.add(tag);
				} catch (Exception e) {
					throw new IOException(e);
				}
			}
			return relations;
		} catch (SAXException e) {
			return relations;
		} catch (ParserConfigurationException e) {
			throw new IOException(e);
		}
	}

	@Override
	public void setCredentials(final String userName, final String password) {
		this.userName = userName;
		this.password = password;
	}
	
	/**
	 * Method opens a connection and parses the retrieved InputStream with a JAXP parser.
	 * @return The from the parse call returned Document Object
	 * @throws IOException
	 * @throws SAXException 
	 * @throws ParserConfigurationException 
	 */
	private Document getDocument() throws IOException, SAXException, ParserConfigurationException {
		InputStream inputStream = null;
		try {
			final URLConnection connection = apiURL.openConnection();
			connection.setRequestProperty(HEADER_USER_AGENT, userAgent);
			connection.setRequestProperty(HEADER_AUTHORIZATION, encodeForAuthorization());
			inputStream = connection.getInputStream();
			/*
			 * get the content encoding fall back to UTF-8
			 */
			final String encoding = connection.getContentEncoding();
			return this.parseInputStream(inputStream, present(encoding) ? encoding : DELICIOUS_DEFAULT_ENCODING);
		} finally {
			if (inputStream != null) {
				inputStream.close();
			}
		}
	}
	
	private Document parseInputStream(final InputStream inputStream, final String encoding) throws ParserConfigurationException, SAXException, IOException {
		// Get a JAXP parser factory object
		final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		// Tell the factory what kind of parser we want 
		dbf.setValidating(false);
		// Use the factory to get a JAXP parser object

		final DocumentBuilder parser = dbf.newDocumentBuilder();

		// Tell the parser how to handle errors.  Note that in the JAXP API,
		// DOM parsers rely on the SAX API for error handling
		parser.setErrorHandler(new ErrorHandler() {
			@Override
			public void warning(SAXParseException e) {
				log.warn(e);
			}
			@Override
			public void error(SAXParseException e) {
				log.error(e);
			}
			@Override
			public void fatalError(SAXParseException e)
					throws SAXException {
				log.fatal(e);
				throw e;   // re-throw the error
			}
		});
		
		// Finally, use the JAXP parser to parse the file.  
		// This call returns a Document object. 
		final InputSource source = new InputSource(new FilterInvalidXMLCharsReader(new InputStreamReader(inputStream, encoding)));
		return parser.parse(source);
	}
	
	/**
	 * Encode the username and password for BASIC authentication
	 * 
	 * @return Basic + Base64 encoded(username + ':' + password)
	 */
	protected String encodeForAuthorization() {
		try {
			return HEADER_AUTH_BASIC + new String(Base64.encodeBase64((this.userName + ":" + this.password).getBytes()), DELICIOUS_DEFAULT_ENCODING);
		} catch (UnsupportedEncodingException e) {
			return HEADER_AUTH_BASIC + new String(Base64.encodeBase64((this.userName + ":" + this.password).getBytes()));
		}
	}
	
}

